package ar.edu.itba.pod.legajo48240.node;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import org.apache.log4j.Logger;

import ar.edu.itba.balance.api.AgentsBalancer;
import ar.edu.itba.balance.api.NodeAgent;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.node.api.ClusterAdministration;
import ar.edu.itba.pod.legajo48240.model.Host;
import ar.edu.itba.pod.legajo48240.utils.Net;
import ar.edu.itba.pod.legajo48240.utils.Utils;

public class ClusterManager extends UnicastRemoteObject implements
		ClusterAdministration {

	private static final long serialVersionUID = 1L;

	private String groupid;
	
	private volatile Set<NodeInformation> group=new HashSet<NodeInformation>();
	
	private final NodeInformation source;
	
	private final Host localhost;

	private static final Logger LOGGER = Logger.getLogger(ClusterManager.class);

	public ClusterManager(Host host) throws RemoteException {
		this.source = host.node();
		this.localhost= host;
	}

	// Local
	@Override
	public void createGroup() throws RemoteException {
		if (isConnectedToGroup())
			throw new IllegalStateException();
		group.add(source);
		this.groupid = "CLUSTER " + source.id();
		localhost.setCoordinator(localhost.node());
//		localhost.balancer().bullyElection(localhost.node(), System.nanoTime());
	}

	// Local
	@Override
	public String getGroupId() throws RemoteException {
		return (!isConnectedToGroup()) ? null : groupid;
	}

	// Local
	@Override
	public boolean isConnectedToGroup() throws RemoteException {
		return (!group.isEmpty());
	}

	@Override
	public void connectToGroup(String targetHost, int targetPort) throws RemoteException,
			NotBoundException {
		
		LOGGER.info("connect");
		if (!isConnectedToGroup()) {

			LOGGER.info("connect " + targetHost + " "  +targetPort);
			ClusterAdministration remoteHost=null;
			try{
				remoteHost= Net.getCluster(targetHost, targetPort);
				LOGGER.info("connect donde estas?");
			}catch (Exception e) {
				e.printStackTrace();
			}
			Set<NodeInformation> copy = Utils.clone(remoteHost.connectedNodes());
			
			LOGGER.info("connect");
			if (!copy.contains(source)) {
				copy=remoteHost.addNewNode(source);
			}
			
			LOGGER.info("connect");
			
			for(NodeInformation remote: copy){
//				if(!remote.equals(source)){
					
					/** add remote nodeInformation to local cluster, also add another conection
					 *  to enforce cluster connections
					 *  1- me
					 *  2- target
					 *  3- another 
					 */
					
					if( (remote.host().equals(targetHost) && remote.port()==targetPort && 
							!localhost.network().connectedNodes().contains(remote) )
							||
							((!remote.host().equals(targetHost) || remote.port()!=targetPort ) &&  
							localhost.network().connectedNodes().size()<=3 && 
							!localhost.network().connectedNodes().contains(remote)) 
							){
						
						localhost.network().addNewNode(remote);
//						LOGGER.debug(source.id() + " add node " +  remote.id());
//					}
				}
			}
			LOGGER.info(source.id() + " Connected nodes: " + remoteHost.connectedNodes());

			// sends new election
//			localhost.balancer().bullyElection(localhost.node(), System.nanoTime());
			
			LOGGER.info("connect");
			
//			for(NodeInformation node: localhost.network().connectedNodes()){
//				AgentsBalancer balancer=Net.getBalancer(node);
//				balancer.bullyElection(localhost.node(), System.nanoTime());
//			}
			
			Net.getBalancer(localhost.node()).bullyElection(localhost.node(), System.nanoTime());

		}
	}

	@Override
	public void disconnectFromGroup(NodeInformation node)
			throws RemoteException, NotBoundException {

		Queue<NodeInformation> deadNode = new LinkedList<NodeInformation>();
		deadNode.offer(node);
		
		NodeInformation deadOne;
		
		while((deadOne=deadNode.poll())!=null){
			
			if (isConnectedToGroup() && group.contains(node)) {
				LOGGER.info("DISCONNECTING... " + node.id() + " from " + localhost.node().id() + "...");
				group.remove(node);
//				localhost.shutdown();
				
				for (NodeInformation n : localhost.network().connectedNodes()) {
					if(!n.equals(source)){
						try{
//							List<NodeAgent> agents = Net.getTransferer(n).stopAndGet(Net.getTransferer(n).getNumberOfAgents());
//							Net.getBalancer(n).shutdown(agents);
							Net.getCluster(n.host(), n.port()).disconnectFromGroup(node);
						} catch (Exception e) {
							deadNode.offer(n);
						}
					}
				}
			}
		}
			

	}

	@Override
	public Set<NodeInformation> addNewNode(NodeInformation node)
			throws RemoteException, NotBoundException {

			/** this is for thread safe use of collection */
			/** this is for not broadcast to the sender itself */
		
			Set<NodeInformation> copy = connectedNodes();
			
			
			if (!group.contains(node)) {
				LOGGER.info("added " + node.id() + " in " + source.id() + " copy " + copy );
				group.add(node);
			}
			
			for (NodeInformation n: copy) {
				
				if(!(n.equals(source))){ // ?? 
					
					try{
						ClusterAdministration remoteHost = Net.getCluster(n.host(), n.port());
						if(!remoteHost.connectedNodes().contains(node)){
							LOGGER.info("added " + n );
							remoteHost.addNewNode(node);
						}
					}	catch (NotBoundException e){ 
						LOGGER.warn("Dead connection node");
						disconnectFromGroup(n);
					}
				}
			}
			
		return Utils.clone(group);
	}

	@Override
	public Set<NodeInformation> connectedNodes() throws RemoteException {
		return Utils.clone(group);
	}

}
